Assembly Language
©
Copyright Brian Brown, 1988-2000. All rights reserved.
| Notes | Home Page |
REAL TIME EXECUTIVES
OVERVIEW
This article deals with the development of a real-time
executive for PC applications. The executive developed is based on a pre-emptive
round-robin queue, and supports multiple tasks within a global programs
address/data space.
A large memory model Turbo C program is split into a number of independent or co-operating tasks, each task being inserted into a round robin queue. The processors real-time clock is redirected to a scheduler, part of the executive, which pre-empts the running process and switches to a waiting task.
The executive supplies system calls for suspending and waking up a task, as well as task intercommunication via messages.
The executive was developed as a teaching aid to software engineering students. As such, it illustrates the operation of the kernel at first hand, allowing students to modify and test its behavior. A simple kernel was essential, which permits student familiarization quickly and efficiently.
Initially, a co-operative scheduler is developed. This scheduler is easy to implement as each task voluntarily releases the processor. Later, the scheduler is changed to a pre-emptive version, which allows simultaneous running tasks, with a block/wakeup message passing system.
Task Scheduling, Priority, Suspension and Wakeup
Scheduling refers
to deciding which waiting task should be run next by the processor when it
becomes free. This implies that each task being executed by the processor has a
task state associated with it.
Other task states will be added later. A processor is switched from one task to another either voluntarily or by pre-emption. To restart a task which has yielded the processor, the information needed is,
Often, a task is required to execute more often than other tasks in the system (ie, receive more processor time). This means each task should have a task priority. When the decision is being made concerning the next task to run, higher priority tasks will be considered first. This means that a tasks priority could dynamically change throughout its lifetime.
A simple scheduling system is the round-robin queue, where each task has equal priority and are inserted into a queue. The task at the front of the queue receives the processor when it becomes free. A task releasing the processor enters the rear of the queue, gradually working its way to the top.
Often, a task will perform some function (like a compile or print operation) which relies upon the functions completion before it can continue. In this case, rather than waste processor time executing a task which cannot continue, the task state is marked as blocked, and the task is not considered when the processor becomes available.
This can be done by removing the task from the processor queue, or modifying the scheduling algorithm so that tasks flagged as blocked are not considered for assignment to the processor. Blocking a task is often called task suspension.
When the event upon which a blocked task is waiting for occurs (eg, printer has finished), its task state is changed to ready and it is reinserted into the ready queue. This is referred to as task wakeup.
Once a new task has been decided upon, the processor must be made to run the selected task. This is called dispatching a task. The processor registers are updated to reflect the new task. The act of switching from one task to another is called context switching.
To manage each task in a computer system, the kernel keeps a table for each task. This is called a process control block (PCB). Its definition in C could look like,
struct task { unsigned int SS; unsigned int SP; unsigned int process_state; unsigned int process_priority; } PROCESS[NUM_TASKS];
Each task in the computer system also has its own stack space, so these definitions look like,
#define STACK_LEN 1025 unsigned int stack_main[STACK_LEN]; unsigned int stack_task1[STACK_LEN]; unsigned int stack_task2[STACK_LEN];
In a multi-user system, the memory for the tasks code, data area and stack space would be allocated by the memory management functions of the kernel, rather than globally declared (which occurs in this example and small control type systems).
The kernel requires some extra information to keep track of the current task.
int task_number;
Consider the arrival of a new task in the system. It is allocated memory to run, added to the ready queue and its PCB filled in. In this example, the ready queue is an array of PCB's, one entry per task (maximum of three tasks). A function which performs this looks like,
void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)()) { /* insert task details into task PROCESS table */ PROCESS[tasknum].SS = (unsigned int) FP_SEG( stackspace ); PROCESS[tasknum].SP = (unsigned int) FP_OFF( &stackspace[STACK_LEN-2]); PROCESS[tasknum].process_state = READY; PROCESS[tasknum].process_priority = NORMAL_PRIORITY; stackspace[STACK_LEN-2] = (unsigned int) FP_OFF( fptr ); stackspace[STACK_LEN-1] = (unsigned int) FP_SEG( fptr ); /* contained at the top of the stack is the entry address into the task */ }
This function inserts the pointer to the tasks stack space into the PROCESS table, creating a PCB entry for the task. It accepts the entry point to the task, a pointer to its stack space, and the entry slot into the PROCESS array.
It loads the tasks PCB stack registers with the address of the stack space for the task, and inserts the entry address of the task onto the top of the stack for that task. This means that a RET instruction will execute this task if the processors stack registers are altered to point to the tasks stack space!
The kernel inserts the new tasks with a function call which looks like,
newprocess( 0, stack_main, &main ); newprocess( 1, stack_task1, &task1 ); newprocess( 2, stack_task2, &task2 );
In a non-pre-emptive scheduling system, a task releases the processor voluntarily. It does this by calling a kernel function. We have called this function transfer, which switches to another ready task. Its purpose is to save the current tasks stack registers, and select the next task for execution. Having selected a task, it then switches the processors stack registers to point to the tasks stack space, and executes a RETurn instruction.
It changes the releasing tasks state from RUNNING to READY, and updates the new tasks state from READY to RUNNING. Inside the function, the scheduler skips tasks which are blocked.
void far transfer( void ) { disable(); /* save current running task */ PROCESS[task_number].SS = (unsigned int) _SS; PROCESS[task_number].SP = (unsigned int) _SP; if( PROCESS[task_number].process_state == RUNNING) PROCESS[task_number].process_state = READY; /* set up for new task */ task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; while(PROCESS[task_number].process_state == BLOCKED) task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; _SS = PROCESS[task_number].SS; /* _SP = PROCESS[task_number].SP; This statement must be done is assembler else the Compiler will try to use the stack to generate a valid _ES reference. As SS has been adjusted, BUT NOT SP, this goes into nomans land. */ asm mov sp, word ptr es:[bx+2]; /* PROCESS[task_number].process_state = RUNNING; */ asm mov word ptr es:[bx+4], RUNNING; enable(); }
Please note the need for assembler within the scheduler to adjust the stack pointer to the new task. It is important not to change the order of the fields within the structure PROCESS, as it assumes that the SP value is located at offset 2 and the task state at offset 4.
The only remaining functions handle task suspension and wakeup. A task is suspended using block, whilst a task is resumed using wakeup. These functions change the calling tasks state, though in multi-user systems would also involve the use of secondary storage media storing blocked tasks on disk rather than in main memory).
A blocked task is woken up by another task or the kernel. Because it does not receive processor time, it cannot wake itself up.
void block(void) { PROCESS[task_number].process_state = BLOCKED; transfer(); } void wakeup(int task_num) { PROCESS[task_num].process_state = READY; }
One other requirement is for tasks to sleep for a specified period. A kernel function called sleep provides this, which accepts a tickrate as a parameter. This changes the calling tasks state to SLEEPING. This necessitates a small change to the definition of a tasks PCB.
#define SLEEPING 3 struct task { unsigned int SS; unsigned int SP; unsigned int process_state; unsigned int process_priority; unsigned int sleep_rate; } PROCESS[NUM_TASKS];
The newprocess function has the following line added to initialize the sleep_rate variable for each task.
void newprocess( .... ) { ...... PROCESS[task_number].sleep_rate = 0; ...... }
The transfer function is altered to take into account sleeping tasks, by decrementing their sleep_rate count each time its called. The problem with this co-operative implementation is that it really does not work, since a task might never call transfer and release the processor. However, when the code is changed to be pre-emptive, this problem disappears.
#define PTNPS PROCESS[task_number].process_state void far transfer( void ) { disable(); ...... /* save current running task */ /* set up for new task */ task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; while( (PTNPS == BLOCKED) || (PTNPS == SLEEPING ) ) { if( PTNPS == SLEEPING) if( (PROCESS[task_number].sleep_rate--) == 0) PTNPS = READY; task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; } ...... }
A running task calls the sleep function to delay itself. This changes its process state to sleeping and initiates a transfer to another ready task. The sleep function looks like
void sleep( int delay ) { PROCESS[task_number].process_state = SLEEPING; PROCESS[task_number].sleep_rate = delay; transfer(); }
Task Intercommunication
Tasks intercommunicate by sending messages,
data or code between them. Thus, each task has a receive buffer and some task
flag associated with sending and receiving of messages.
The following addition is made to the definition of a tasks PCB.
struct task { ....... int msg; char far *msgbuf; } PROCESS[NUM_TASKS];
The following addition is made to function newprocess.
void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)(), char far *msgbuffer) { ...... ...... PROCESS[tasknum].msg = FALSE; PROCESS[tasknum].msgbuf = msgbuffer; }
Sendmessage is a kernel function that accepts a destination task_number and a pointer to the message being sent. It copies the message into the receiving tasks message buffer and signals the receiving task that a message has arrived.
#define MAXMSGLEN 80 void far sendmessage( int task_number, char far *msg ) { int count = 0; char far *rbuf; disable(); /* don't interrupt this */ rbuf = PTN(msgbuf); /* address of tasks recbuf */ while( (*msg) && (count < MAXMSGLEN)) { /* while message is not null*/ count++; /* and count is less than */ *rbuf = *msg; /* the size of a recbuffer */ rbuf++; msg++; /* copy from sender to */ } /* receiver */ *rbuf = '\0'; /* NULL terminate recbuffer */ PTN(msg) = TRUE; /* signal message arrival */ enable(); /* re-enable interrupts now */ }
receivemessage is a kernel function that a running task calls to receives a message. If no message has arrived, the task waits (is blocked) till the message arrives. Upon receipt of the message, the function returns with the data in the receive buffer for the calling task.
char far *receivemessage(void) { if( PROCESS[task_number].msg == FALSE ) block(); return PROCESS[task_number].msgbuf; }
The following change is made to process transfer to allow messages and the unblocking of tasks waiting for a message.
#define PTNPS PROCESS[task_number].process_state void far transfer(void) { disable(); ...... /* save current running task */ /* set up for new task */ task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; while( (PTNPS == BLOCKED) || (PTNPS == SLEEPING ) ) { if( PTNPS == SLEEPING) if( (PROCESS[task_number].sleep_rate--) == 0) PTNPS = READY; if( (PTNPS == BLOCKED) && (PROCESS[task_number].msg == TRUE) ) PTNPS = READY; task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; } PROCESS[task_number].process_state = RUNNING; ...... }
Task Synchronization
The block, wakeup, sleep, sendmessage and
receivemessage functions can be used by tasks to synchronize their activity.
CHANGING TO A PRE-EMPTIVE SCHEDULER
Developing a pre-emptive
version is not that difficult, since much of the ground work has been correctly
done. The scheduler must be interrupt driven, so we will hook it into the real
time clock (RTC) of the PC which is executed 18.2 times a second.
Changing the function transfer to an interrupt type and placing its address into the interrupt vector associated with the RTC are the essential changes.
When an interrupt occurs, all the processor registers except SS and SP are saved. The scheduler will terminate by swapping the stack, but instead of using a return statement, it will execute a return from interrupt instruction. This will unstack all the registers. The newprocess function will need to simulate an interrupt return instruction on the top of each tasks stack frame. The necessity of saving the BP register within the PCB of a task is now eliminated. This was necessary before to keep track of each tasks local variables, which are accessed via the BP register.
A global variable is used to keep the address stored in the old RTC vector, as this will be restored when exiting to DOS. Failure to restore this will result in a reboot.
The RTC vector is saved, and then the scheduler address is inserted. This must be done with interrupts disabled, to prevent the scheduler becoming active before the kernel has a chance to set up everything. This vector is altered once all the tasks have been initialised by calls to new_process().
void far interrupt (*oldtimer)(); oldtimer = getvect( 0x08 ); /* save old RTC interrupt */ disable(); /* stop interrupts */ setuptimer( transfer ); /* hook scheduler into RTC */ enable(); /* and now its away */
Upon exit to DOS, the system must be restored to normal. This is achieved by either the function ctc or myexit.
disable(); /* turn off interrupts */ setvect( 0x08, oldtimer ); /* restore original timer */ outportb( 0x43, 0x36 ); /* reset 8253 channel 0 to mode 3 */ outportb( 0x40, 0x00 ); outportb( 0x40, 0x00 ); enable(); /* reenable interrupts */
The scheduler is changed to type interrupt. This terminates the function with an IRET instruction. Before the function terminates, the last statement resets the 8259 Priority Interrupt Controller (PIC) chip to allow further interrupts. If this is not done, the scheduler is invoked only once. Writing a value of 0x20 to the PIC permits further interrupts to be processed.
#define PIC 0x20 /* Priority Interrupter Controller address */ #define EOI 0x20 /* End Of Interrupt signal code for PIC */ void far interrupt transfer(void) { ..... outportb( PIC, EOI ); }
The function newprocess is altered to create a dummy interrupt state on the top of the tasks stack space. This is necessary, as the scheduler switches to a tasks stack space, then executes an IRET statement.
void far newprocess( int task_number, unsigned int stack[], void far (*fptr)(), char far *msgbuffer ) { ..... stack[STACK_LEN - 2] = (unsigned int) 0x0200; /* insert a dummy flag value, interrupts enabled */ stack[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr ); /* place CS:IP of task into tasks stack space */ stack[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr ); stack[STACK_LEN - 5] = _AX; stack[STACK_LEN - 6] = _BX; stack[STACK_LEN - 7] = _CX; stack[STACK_LEN - 8] = _DX; stack[STACK_LEN - 9] = _DS; /* _ES */ stack[STACK_LEN -10] = _DS; /* _DS */ stack[STACK_LEN -11] = _SI; stack[STACK_LEN -12] = _DI; stack[STACK_LEN -13] = _BP; }
The only other changes are necessary to the block, wakeup, sleep and sendmessage functions. Because the scheduler can interrupt at any time, code which alters the state of any PROCESS fields should do so with interrupts disabled. Thus these functions begin with the statement disable and end with enable. The functions block and sleep must invoke the scheduler via an interrupt call, they do this by waiting for an interrupt to occur (using the HLT instruction).
void far sleep(unsigned int clockticks) { disable(); ...... enable(); asm hlt; /* interrupt call to transfer(); */ }
One final function is called setuptimer, which inserts the address of the scheduler into the RTC interrupt vector. It also reprograms channel 0 of the 8253 to interrupt at a rate of 72 times per second (0x4000). The scheduler frequency is easily altered by changing the last two bytes output to the 8253, which are the LSB and MSB of a divisor. Values of 0x0000 will generate a frequency rate of 18.2 times per second, a divisor of 0x8000 will generate a rate of 144 times per second.
void far setuptimer( void interrupt far (*switcher)() ) { setvect( 0x08, switcher ); /* link into RTC interrupt 0x08 */ outportb( 0x43, 0x34 ); /* reprogram 8253, mode 2 channel 0 */ outportb( 0x40, 0x00 ); outportb( 0x40, 0x40 ); }
Taking over the RTC interrupt causes two things. Firstly, the DOS time of day will not function, and the motor time out for floppy disk drives will not work.
The pre-emptive scheduler has a serious restriction. Since DOS is not re-entrant, each task must not call DOS. It is possible for one task to use a particular DOS call, then be interrupted by another task trying to use the same DOS call. If this occurs, the system will hang. As the executive is designed for students to create NON-DOS dependant programs (control, data logging, embedded systems), this restrictions is acceptable.
The overhead of the scheduler was timed at 22 microseconds on a 386 running at 20mhz. With a switching rate of 18.2 times per seond, this means each task runs for approximately 54.89 milliseconds before being interrupted. This gives a system overhead of 4 percent.
Listing TWO contains the code for the pre-emptive scheduler, including task suspension, wakeup and message passing functions.
A RUDIMENTARY DOS PRE-EMPTIVE SCHEDULER
So far, we have covered two
basic schedulers, co-operative and pre-emptive. The pre-emptive version is
effecient and simple. Its only drawback is the inability of tasks to use DOS
type functions. We shall now convert the scheduler to support DOS video
functions.
We need some method of telling the scheduler not to switch tasks when it discovers the task just interrupted was executing a DOS call. In this way, the task will not be switched, and will continue till the DOS call is finished.
To accomplish this, two flags are used. The flags are
InVideoBios INDOS
InVideoBios keeps track of calls to the INT10h vector which handles ROMBIOS calls to the video screen. It is handled by a function which is inserted into the int10 vector, replacing the system one. This function looks like
void interrupt far newint10( void ) { ++InVideoBios; (*oldint10)(); --InVideoBios; outportb( PIC, EOI); }
When a task calls the int10 routines (via DOS), the InVideoBios flag is incremented, and control is passed to the original vector. Upon return, the flag is decremented and the Priority Interrupt Controller (PIC) is reset to allow further processing.
The other flag is a system variable kept by DOS to indicate to itself when system calls are in progress. The address of this variable is obtained using a system call to DOS of type 0x34.
_AH = 0x34; /* get pointer to INDOS flag */ geninterrupt( 0x21 ); INDOS = MK_FP( _ES, _BX );
The scheduler is altered to prevent switching from a task currently in DOS.
void far interrupt scheduler( void ) { if( *INDOS || InVideoBios ) { /* don't switch if in DOS */ outportb( PIC, EOI ); return; /* go back to interrupted task */ } ..... ..... }
These changes now allow a task to use the printf statement. Listing THREE is a code example of the DOS pre-emptive scheduler.
It should be noted that to be bullet proof, the DOS scheduler should also take over other interrupts which might cause problems (keyboard, disk access). We have shown how to get it running using video calls, the rest is up to you.
SOFTWARE LISTINGS
Listing
1
Co-operative scheduler
Listing
2
Pre-emptive scheduler
Listing 4
Embedded system for PC-DIO card/panel using
pre-emptive scheduler
LISTING 1 task.c a co-operative scheduler for the PC /* task1.c, co-operative round-robin scheduler Copyright Brian Brown, CIT, 28th September 1989 All rights reserved. To compile this program type, tcc -N- -B -v- -r- -y- -k- -a -K -ml -p- -Ic:\turbo\include -Lc:\turbo\lib task1.c each task has its own stack space each tasks information is stored into PROCESS using newprocess() a task transfers to another by calling transfer() each task can use DOS routines each task can use local variables */ #include <stdio.h> #include <dos.h> #define NUM_TASKS 3 #define STACK_LEN 1025 #define READY 0x0000 #define RUNNING 0x0001 #define BLOCKED 0x0002 extern unsigned _stklen = 8192; struct task { unsigned int SS; unsigned int SP; unsigned int BP; unsigned int process_state; } PROCESS[NUM_TASKS]; unsigned int stack_main[STACK_LEN]; unsigned int stack_task1[STACK_LEN]; unsigned int stack_task2[STACK_LEN]; int task_number; void far transfer( void ) { disable(); PROCESS[task_number].SS = (unsigned int) _SS; /* save current running task */ PROCESS[task_number].SP = (unsigned int) _SP; PROCESS[task_number].BP = (unsigned int) _BP; PROCESS[task_number].process_state = READY; /* set up for new task */ task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; _SS = PROCESS[task_number].SS; /* _SP = PROCESS[task_number].SP; This statement must be done is assembler else the C Compiler will try to use the stack to generate a valid _ES reference. As SS has been adjusted, BUT NOT SP, this goes into nomans land. */ asm mov sp, word ptr es:[bx+2]; /* _SP = PROCESS[task_number].SP */ asm mov bp, word ptr es:[bx+4]; /* _BP = PROCESS[task_number].BP */ asm mov word ptr es:[bx+6], RUNNING; /* PROCESS[task_number].process_state = RUNNING */ enable(); } void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)()) { /* insert task details into task PROCESS table */ PROCESS[tasknum].SS = (unsigned int) FP_SEG( stackspace ); PROCESS[tasknum].SP = (unsigned int) FP_OFF( &stackspace[STACK_LEN-2]); PROCESS[tasknum].process_state = READY; stackspace[STACK_LEN-2] = (unsigned int) FP_OFF( fptr ); stackspace[STACK_LEN-1] = (unsigned int) FP_SEG( fptr ); /* contained at the top of the stack is the entry address into the task */ } void far task1( void ) { int i; i = 0; while( 1 ) { printf("Task1: i = %d\n", i); i += 1; transfer(); } } void far task2( void ) { int i; i = 0; while( 1 ) { printf("Task2: i = %d\n", i); i += 2; transfer(); } } void far main() { int i; i = 0; task_number=0; /* initialise the task PROCESS table */ newprocess( 0, stack_main, &main ); newprocess( 1, stack_task1, &task1 ); newprocess( 2, stack_task2, &task2 ); while( 1 ) { printf("main: i = %d\n", i); i += 3; transfer(); } }
LISTING 2 task2.c a pre-emptive scheduler for PC /* task.c, interrupt driven round robin scheduler Copyright Brian Brown, CIT, 28th September 1989 All rights reserved. To compile this program type, tcc -N- -B -v- -r- -y- -k- -a -K -ml -p- -Ic:\turbo\include -Lc:\turbo\lib task2.c each task MUST NOT call DOS, each task is treated equally */ #include <stdio.h> #include <dos.h> #include <conio.h> #include <alloc.h> #define TRUE 1 #define FALSE 0 #define NUM_TASKS 3 /* number of tasks in system 0,1,2 */ #define READY 0 /* waiting for the processor */ #define RUNNING 1 /* currently has the processor */ #define BLOCKED 2 /* waiting for message */ #define SLEEPING 3 /* asleep, cannot run */ #define NORMAL_PRIORITY 7 #define HIGH_PRIORITY 1 #define LOW_PRIORITY 14 #define STACK_LEN 1025 /* length of stack for a task */ #define F1 0x3b /* function key F1 code */ #define PIC 0x20 /* Priority Interrupt controller */ #define EOI 0x20 /* end of interrupt for PIC */ #define MAXMSGLEN 80 /* maximum length of a message */ #define PTN(x) PROCESS[task_number].x struct task { /* process control block for tasks */ unsigned int SS; /* stack segment register */ unsigned int SP; /* stack pointer for each task */ unsigned int process_state; /* task state */ unsigned int process_priority; /* task priority */ unsigned int sleep_rate; /* task delay time in ticks */ int msg; /* true if message arrived */ char far *msgbuf; /* pointer to message space */ unsigned int clock_ticks; /* number of timeslices per task */ } PROCESS[NUM_TASKS]; /* one entry per known task */ extern unsigned _stklen = 8192; unsigned int stack_main[STACK_LEN]; /* stack for main task */ unsigned int stack_task1[STACK_LEN]; /* stack for task1 */ unsigned int stack_task2[STACK_LEN]; /* stack for task2 */ unsigned char msg_main[MAXMSGLEN+1]; /* message buffer for main task */ unsigned char msg_task1[MAXMSGLEN+1]; /* message buffer for task1 */ unsigned char msg_task2[MAXMSGLEN+1]; /* message buffer for task2 */ unsigned int task_number; /* holds current running task ID */ void far interrupt (*oldtimer)(); /* holds old int8h vector */ unsigned int x,y,offset; /* used by print routine */ void far sleep(unsigned int clockticks) /* voluntary task sleep */ { disable(); /* turn off interrupts */ PTN(process_state) = SLEEPING; /* change task state to SLEEPING */ PTN(sleep_rate) = clockticks; /* fill in duration to sleep */ enable(); /* re-enable interrupts */ asm hlt; /* interrupt call to transfer(); */ } void far block( void ) /* block a task */ { disable(); /* turn off interrupts */ PTN(process_state) = BLOCKED; /* change task state */ enable(); /* re-enable interrupts */ asm hlt; /* wait for transfer */ } void far wakeup( int task_number ) /* unblock a task */ { disable(); /* turn off interrupts */ PTN(process_state) = READY; /* change task state */ enable(); /* re-enable interrupts */ } void far sendmessage( int task_number, char far *msg ) { int count = 0; char far *rbuf; disable(); /* don't interrupt this */ rbuf = PTN(msgbuf); /* address of tasks recbuf */ while( (*msg) && (count < MAXMSGLEN)) { /* while message is not null*/ count++; /* and count is less than */ *rbuf = *msg; /* the size of a recbuffer */ rbuf++; msg++; /* copy from sender to */ } /* receiver */ *rbuf = '\0'; /* NULL terminate recbuffer */ PTN(msg) = TRUE; /* signal message arrival */ enable(); /* re-enable interrupts now */ } char far * recievemessage( void ) { if( PTN(msg) == FALSE ) block(); /* wait for message arrival */ return PTN(msgbuf); /* return pointer to buffer */ } void far setuptimer( void interrupt far (*switcher)() ) { setvect( 0x08, switcher ); /* link into RTC interrupt 0x08 */ outportb( 0x43, 0x34 ); /* reprogram 8253, mode 2 channel 0 */ outportb( 0x40, 0x00 ); outportb( 0x40, 0x40 ); } void far interrupt transfer( void ) { PTN(SS) = (unsigned int) _SS; /* save current running task stack pointers*/ PTN(SP) = (unsigned int) _SP; if( PTN(process_state) == RUNNING) /* if a running task, change to ready*/ PTN(process_state) = READY; task_number++; /* select next task to run */ if( task_number > (NUM_TASKS-1) ) task_number = 0; /* skip tasks which are blocked and sleeping. for sleeping tasks decrement their sleep_rate count, and if zero, alter their task state to READY. For blocked tasks, see if they have recieved a message, and if they have, change their task state to READY */ while((PTN(process_state) == BLOCKED)||(PTN(process_state) == SLEEPING)) { if( PTN(process_state) == SLEEPING) { PTN(sleep_rate--); if( PTN(sleep_rate) == 0) wakeup( task_number ); } if((PTN(process_state) == BLOCKED) && (PTN(msg) == TRUE)) { PTN(process_state) = READY; PTN(msg) = FALSE; } task_number++; if( task_number > (NUM_TASKS-1) ) task_number = 0; } _SS = PTN(SS); /* swap to stack of newly selected task */ /* _SP = PTN(SP); This statement must be done is assembler else the C Compiler will try to use the stack to generate a valid _ES reference. As SS has been adjusted, BUT NOT SP, this goes into nomans land. */ asm mov sp, word ptr es:[bx+2]; asm mov word ptr es:[bx+4], RUNNING; /* PTN(process_state) = RUNNING; */ asm inc word ptr es:[bx+16]; /* PTN(clock_ticks)++; */ outportb( PIC, EOI ); /* signal EOI to 8239 PIC */ } void far newprocess( task_number, stack, fptr, msgbuffer ) int task_number; unsigned int stack[]; void far (*fptr)(); char far *msgbuffer; { /* inserts a task into PROCESS table, simulates an INT call on the top of its stack space */ /* save SS:SP address of tasks stack space in PROCESS table */ PTN(SS) = (unsigned int) FP_SEG( stack ); PTN(SP) = (unsigned int) FP_OFF( &stack[STACK_LEN -13]); PTN(process_state) = READY; PTN(process_priority) = NORMAL_PRIORITY; PTN(sleep_rate) = 0; PTN(msg) = FALSE; PTN(msgbuf) = msgbuffer; PTN(clock_ticks) = 0; stack[STACK_LEN - 2] = (unsigned int) 0x0200; /* insert a dummy flag value, interrupts enabled */ stack[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr ); /* place CS:IP of task into tasks stack space */ stack[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr ); stack[STACK_LEN - 5] = _AX; stack[STACK_LEN - 6] = _BX; stack[STACK_LEN - 7] = _CX; stack[STACK_LEN - 8] = _DX; stack[STACK_LEN - 9] = _DS; /* _ES */ stack[STACK_LEN -10] = _DS; /* _DS */ stack[STACK_LEN -11] = _SI; stack[STACK_LEN -12] = _DI; stack[STACK_LEN -13] = _BP; } int far ctc( void ) /* ctrlc handler */ { disable(); /* turn off interrupts */ setvect( 0x08, oldtimer ); /* restore original timer */ outportb( 0x43, 0x36 ); /* reset 8253 channel 0 to mode 3 */ outportb( 0x40, 0x00 ); outportb( 0x40, 0x00 ); enable(); /* reenable interrupts */ exit( 0 ); /* exit to DOS */ } void far myexit( int exit_code ) { int i; disable(); /* turn off interrupts */ setvect( 0x08, oldtimer ); /* restore original timer */ outportb( 0x43, 0x36 ); /* reset 8253 channel 0 to mode 3 */ outportb( 0x40, 0x00 ); outportb( 0x40, 0x00 ); enable(); /* reenable interrupts */ for( i = 0; i < NUM_TASKS; i++ ) printf("\nTask %d received %u clock_ticks", i, PROCESS[i].clock_ticks); exit( exit_code ); } void far print( char *stringptr ) { while( *stringptr ) { offset = (x + ( y * 80 )) * 2; pokeb( 0xb800, offset, *stringptr ); x++; if( x > 79 ) { x = 0; y++; if( y > 24 ) { y = 0; x = 0; } } stringptr++; } } void far task1( void ) { unsigned int loop; char far *msg; msg = farcalloc( 10, sizeof(char) ); while( 1 ) { print("task1 "); for( loop = 0; loop < 65535; loop++ ) ; msg[0] = '1'; msg[1] = '\0'; sendmessage(2, msg); } } void far task2( void ) { char far *msg; while( 1 ) { msg = recievemessage(); print("Task2 woken by task1"); } } void far main() { unsigned char ch; unsigned int i; task_number=0; /* current running task is main */ x = 0; y = 0; /* install tasks in PROCESS */ newprocess( 0, stack_main, &main, msg_main ); newprocess( 1, stack_task1, &task1, msg_task1 ); newprocess( 2, stack_task2, &task2, msg_task2 ); setcbrk( 1 ); /* enable for every system call */ ctrlbrk( ctc ); /* redirect CTRL-C */ oldtimer = getvect( 0x08 ); /* save old RTC interrupt */ disable(); /* stop interrupts */ setuptimer( transfer ); /* hook scheduler into RTC */ enable(); /* and now its away */ while( 1 ) { /* this portion of the code checks for F1 key press and if found restores machine state and exits to DOS */ disable(); if( kbhit() != 0 ) { ch = getch(); if( ch == 00 ) { ch = getch(); if( ch == F1 ) myexit(0); } else ungetch(ch); } enable(); /* main task portions now */ print("main "); for( i = 0; i < 32000; i++ ); } }
LISTING 3 task3.c a rudimentary DOS pre-emptive scheduler /* task3.c, interrupt driven round robin scheduler, using DOS routines Copyright Brian Brown, CIT, 28th September 1989 All rights reserved. To compile this program type, tcc -B -v- -r- -y- -k- -a -K -ml -p- -Ic:\turbo\include -Lc:\turbo\lib task3.c */ #include <stdio.h> #include <dos.h> #include <conio.h> #define NUM_TASKS 3 #define STACK_LEN 4096 #define F1 0x3b #define PIC 0x20 #define EOI 0x20 struct task { unsigned int SS; unsigned int SP; } PROCESS[NUM_TASKS]; unsigned int stack_main[STACK_LEN]; unsigned int stack_task1[STACK_LEN]; unsigned int stack_task2[STACK_LEN]; int task_number, x, y, offset, ch; void far interrupt (*oldtimer)(); void far interrupt (*oldint10)(); char far *INDOS; unsigned int InVideoBios; void interrupt far newint10( void ) { ++InVideoBios; (*oldint10)(); --InVideoBios; outportb( PIC, EOI); } void far setuptimer( void interrupt far (*scheduler)() ) { /* link transfer into RTC interrupt 0x08, every 55ms but be careful, interrupt 8h, also drives time of day and disk drive motor control */ setvect( 0x08, scheduler ); } void far interrupt scheduler( void ) { /* interrupts have been disabled, trap disabled */ if( *INDOS || InVideoBios ) { /* don't switch if in DOS */ outportb( PIC, EOI ); return; /* go back to interrupted task */ } PROCESS[task_number].SS = (unsigned int) _SS; /* save current running task */ PROCESS[task_number].SP = (unsigned int) _SP; task_number++; /*set up for new task, switch stacks and restore registers for that task*/ if( task_number > (NUM_TASKS-1) ) task_number = 0; _SS = PROCESS[task_number].SS; /* _SP = PROCESS[task_number].SP; This statement must be done is assembler else the C Compiler will try to use the stack to generate a valid _ES reference. As SS has been adjusted, BUT NOT SP, this goes into nomans land. */ asm mov sp, word ptr es:[bx+2]; outportb( PIC, EOI ); /* signal EOI to 8239 PIC */ } void far newprocess(int tasknum,unsigned int stackspace[],void far (*fptr)()) { /* inserts a task into PROCESS table, and simulates an INT call on the top of its stack space */ /* save SS:SP address of tasks stack space in PROCESS table */ PROCESS[tasknum].SS = (unsigned int) FP_SEG( stackspace ); PROCESS[tasknum].SP = (unsigned int) FP_OFF( &stackspace[STACK_LEN -13]); stackspace[STACK_LEN - 2] = (unsigned int) 0x0200; /* insert a dummy flag value, interrupts enabled */ stackspace[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr ); /* place CS:IP of task into tasks stack space */ stackspace[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr ); stackspace[STACK_LEN - 5] = _AX; stackspace[STACK_LEN - 6] = _BX; stackspace[STACK_LEN - 7] = _CX; stackspace[STACK_LEN - 8] = _DX; stackspace[STACK_LEN - 9] = _DS; /* _ES */ stackspace[STACK_LEN -10] = _DS; /* _DS */ stackspace[STACK_LEN -11] = _SI; stackspace[STACK_LEN -12] = _DI; stackspace[STACK_LEN -13] = _BP; } int far ctc( void ) /* ctrlc handler */ { disable(); setvect( 0x08, oldtimer ); enable(); return 0; } void far task1( void ) { unsigned int z; int i; i = 0; while( 1 ) { printf("task1 = %d\n", i); i += 1; for( z = 1; z < 65000; z++ ); } } void far task2( void ) { unsigned int z; int i; i = 0; while( 1 ) { printf("task2 = %d\n", i); i += 2; for( z = 1; z < 65000; z++ ); } } void far main() { unsigned int z; int i; i = 0; task_number=0; newprocess( 0, stack_main, &main ); newprocess( 1, stack_task1, &task1 ); newprocess( 2, stack_task2, &task2 ); setcbrk( 1 ); /* enable for every system call */ ctrlbrk( ctc ); /* redirect CTRL-C */ _AH = 0x34; /* get pointer to INDOS flag */ geninterrupt( 0x21 ); INDOS = MK_FP( _ES, _BX ); oldint10 = getvect( 0x10 ); /* set up new int10 video */ setvect( 0x10, newint10 ); InVideoBios = 0; oldtimer = getvect( 0x08 ); /* save old timer interrupt */ disable(); setuptimer( scheduler ); /* place scheduler into RTC */ enable(); /* and start it going */ while( 1 ) { disable(); /* this portion allows resetting and exitting back to DOS */ if( kbhit() != 0 ) { /* check for keypress */ ch = getch(); if( ch == 00 ) { /* is it a function key? */ ch = getch(); if( ch == F1 ) { /* yes, then exit program */ disable(); setvect( 0x08, oldtimer ); setvect( 0x10, oldint10 ); clrscr(); enable(); exit(0); } } else ungetch(ch); /* if not a function key, put back */ } /* this is main task portion */ enable(); printf("main = %d\n", i ); i += 3; for( z = 1; z < 65000; z++ ); } }
Listing 4 An embedded application for PC/XT using DIO card/panel CROM.H /* CROM.H, designed by SE2, 1990 for EMBEDDED CODE */ struct eightbit { unsigned char al, ah, bl, bh, cl, ch, dl, dh; }; struct sixteenbit { unsigned int ax, bx, cx, dx, si, di, cflag; }; union REGS { struct sixteenbit x; struct eightbit h; }; /* function prototypes follow */ extern void outportb( unsigned int, char); extern char inportb( unsigned int ); extern void int86( int, union REGS *, union REGS * ); extern void enable( void ); extern void disable( void ); extern void setvect( int interruptnumber, void interrupt (*isr)() ); TCSTART.ASM ; tcstart.asm extrn _main:far _text segment byte public 'CODE' _text ends _textend segment para public 'CODEEND' _textend ends _data segment para public 'DATA' _data ends _dataend segment para public 'DATAEND' _dataend ends _bss segment para public 'BSS' _bss ends _bssend segment byte public 'BSSEND' _bssend ends _stack segment para stack 'STACK' _stack ends DGROUP group _DATA, _DATAEND, _BSS, _BSSEND CGROUP group _TEXT, _TEXTEND _TEXT segment assume CS:CGROUP, DS:DGROUP, ES:DGROUP, SS:_STACK public start db 55h db 0AAh db 40h jmp start start: cli ; disable interrupts mov ax, _STACK ; initialise stack mov ss, ax mov ax, offset stackend mov sp, ax mov ax, seg _BSS mov es, ax mov cx, offset DGROUP:endbss mov di, 0 mov ax, 0 rep stosb ; write to ES:DI mov ax, seg DGROUP mov es, ax ; point ES to _DATA mov cx, offset DGROUP:enddata mov si, 0 mov di, 0 assume ds:CGROUP:_TEXTEND mov ax, seg _TEXTEND:codeend inc ax mov ds, ax ; point DS to _CONST rep movsb ; copy _CONST to _DATA push es ; point DS to _DATA pop ds mov al, 80h ; enable NMI out 0a0h, al mov al, 0bch ; enable 8239 PIC 1011-1100 (irq0,1,6 enabled) out 21h, al sti ; enable interrupts call _main jmp start _TEXT ends _TEXTEND segment public codeend db 16 dup ( ? ) codeend label byte _TEXTEND ends _STACK segment db 1024 dup ('STACK'); stackend label word _STACK ends _BSSEND segment public endbss endbss label byte _BSSEND ends _DATAEND segment public enddata enddata label byte _DATAEND ends end TCLIB.ASM ; TCLIB.ASM, library routine for int86() calls from EMBEDDED CODE public _inportb public _outportb public _enable public _disable public _setvect CODE segment para PUBLIC 'CODE' assume cs:CODE name tclib _enable proc far sti ret _enable endp _disable proc far cli ret _disable endp _setvect proc far push bp ;acess local vars inside functions mov bp,sp ;use bp to acess passed parameters push ax push bx push dx push es mov al, byte ptr [bp+6] ; int number mov ah, 0 rol ax, 1 rol ax, 1 mov bx, ax xor ax, ax mov es, ax mov dx, [bp+10] ; segment mov es:[bx+2], dx mov dx, [bp+8] ; offset mov es:[bx], dx pop es pop dx pop bx pop ax pop bp ret _setvect endp _inportb proc far push bp ;acess local vars inside functions mov bp,sp ;use bp to acess passed parameters push dx mov dx,[bp+6] ; get port address xor ax,ax ; clear ax in al, dx ; read from port into al register mov ah, 00h ; ensure high byte of ax is cleared pop dx pop bp ret _inportb endp _outportb proc far push bp ;acess local vars inside functions mov bp,sp ;use bp to acess passed parameters push dx mov dx,[bp+6] ;get port address mov al, byte ptr [bp+8] out dx, al pop dx pop bp ret _outportb endp CODE ends end TSKPCIO.C /* task.c, interrupt driven round robin scheduler Copyright Brian Brown, CIT, 28th September 1989 All rights reserved. requires DIO card embedded system using multi-tasking */ #include "crom.h" #define FP_OFF(fp) ((unsigned)(fp)) #define FP_SEG(fp) ((unsigned)((unsigned long)(fp) >> 16)) #define TRUE 1 #define FALSE 0 #define NUM_TASKS 3 /* number of tasks in system 0,1,2 */ #define READY 0 /* waiting for the processor */ #define RUNNING 1 /* currently has the processor */ #define STACK_LEN 1025 /* length of stack for a task */ #define PIC 0x20 /* Priority Interrupt controller */ #define EOI 0x20 /* end of interrupt for PIC */ #define PTN(x) PROCESS[task_number].x struct task { /* process control block for tasks */ unsigned int SS; /* stack segment register */ unsigned int SP; /* stack pointer for each task */ unsigned int process_state; /* task state */ } PROCESS[NUM_TASKS]; /* one entry per known task */ extern unsigned _stklen = 8192; unsigned int stack_main[STACK_LEN]; /* stack for main task */ unsigned int stack_task1[STACK_LEN]; /* stack for task1 */ unsigned int stack_task2[STACK_LEN]; /* stack for task2 */ unsigned int task_number; /* holds current running task ID */ void far newprocess( int task_number, unsigned int stack[], void far (*fptr)() ); void far setuptimer( void interrupt far (*switcher)() ) { setvect( 0x08, switcher ); /* link into RTC interrupt 0x08 */ outportb( 0x43, 0x34 ); /* reprogram 8253, mode 2 channel 0 */ outportb( 0x40, 0x00 ); outportb( 0x40, 0x40 ); } void far interrupt transfer( void ) { PTN(SS) = (unsigned int) _SS; /* save current running task stack pointers*/ PTN(SP) = (unsigned int) _SP; PTN(process_state) = READY; task_number++; /* select next task to run */ if( task_number > (NUM_TASKS-1) ) task_number = 0; _SS = PTN(SS); /* swap to stack of newly selected task */ /* _SP = PTN(SP); This statement must be done is assembler else the C Compiler will try to use the stack to generate a valid _ES reference. As SS has been adjusted, BUT NOT SP, this goes into nomans land. */ asm mov sp, word ptr es:[bx+2]; PTN(process_state) = RUNNING; outportb( PIC, EOI ); /* signal EOI to 8239 PIC */ } void far newprocess( task_number, stack, fptr ) int task_number; unsigned int stack[]; void far (*fptr)(); { /* inserts a task into PROCESS table, simulates an INT call on the top of its stack space */ /* save SS:SP address of tasks stack space in PROCESS table */ PTN(SS) = (unsigned int) FP_SEG( stack ); PTN(SP) = (unsigned int) FP_OFF( &stack[STACK_LEN -13]); PTN(process_state) = READY; /* insert a dummy flag value, interrupts enabled */ stack[STACK_LEN - 2] = (unsigned int) 0x0200; /* place CS:IP of task into tasks stack space */ stack[STACK_LEN - 3] = (unsigned int) FP_SEG( fptr ); stack[STACK_LEN - 4] = (unsigned int) FP_OFF( fptr ); stack[STACK_LEN - 5] = _AX; stack[STACK_LEN - 6] = _BX; stack[STACK_LEN - 7] = _CX; stack[STACK_LEN - 8] = _DX; stack[STACK_LEN - 9] = _DS; /* _ES */ stack[STACK_LEN -10] = _DS; /* _DS */ stack[STACK_LEN -11] = _SI; stack[STACK_LEN -12] = _DI; stack[STACK_LEN -13] = _BP; } /* reads switches, transfers to seven segment */ void far task1( void ) { unsigned char sample; while( 1 ) { sample = inportb(0x225); outportb( 0x224, sample ); } } /* rotates leds */ void far task2( void ) { unsigned char ledbyte; unsigned int delay; ledbyte = 1; while( 1 ) { outportb( 0x223, ledbyte ); ledbyte = (ledbyte << 1) & 0xfe; if( (ledbyte & 0xff) == 0 ) ledbyte = 1; for( delay = 1; delay < (unsigned int) 65000; delay++ ) ; } } void far main() { task_number=0; /* current running task is main */ /* install tasks in PROCESS */ newprocess( 0, stack_main, main ); newprocess( 1, stack_task1, task1 ); newprocess( 2, stack_task2, task2 ); outportb(0x227, 0x83); /* initialise DIO card */ outportb(0x223, 0x00); /* turn off all leds */ disable(); /* stop interrupts */ setuptimer( transfer ); /* hook scheduler into RTC */ enable(); /* and now its away */ for( ; ; ) { ; } } TSKPCIO.CFG dup DATA CONST class CODE = 0xd000 class STACK = 0x1000 class DATA = 0x2000 order DATA DATAEND BSS BSSEND order CODE CODEEND CONST rom CODE CONST DOIT.BAT tasm /mx tcstart.asm tasm /mx tclib.asm tcc -a- -c -f- -G- -K -B -ml -M -N- -O- -r- -v- -y- -Z- -S tskpcio.c tlink /m tcstart tskpcio tclib, tskpcio, tskpcio locate tskpcio hexbin2 tskpcio.hex tskpcio.bin i d000